/*
 * Filetransfer.cpp
 *
 *  Created on: 24.06.2011
 *      Author: Mario Hock
 */

#include "Filetransfer.h"
#include "messages/Filetransfer/BundleInfoMessage.h"

#include "Config.h"
#include "UI_multiplexer.h"
#include <iostream>
#include <sstream>
#include <boost/foreach.hpp>


Filetransfer::Filetransfer()
{
}

//** Constructors / Destructors **//
Filetransfer::Filetransfer(Userdatabase* users, Transport* trans) :
    users(users), trans(trans)
{
  trans->register_message_handler(this, TransportMessage::Transfer_Negotiation);
  trans->register_message_handler(this, TransportMessage::Transfer_Payload);
  trans->register_conn_stat_notification(this);

  // FIXME wenn der Filetransfer zu früh instanziiert wird (zB als Singleton), kriegt man hier nur das default...
  unacked_pkg = Config::instance().get_int("omnibus.flow_control");
  
  this->ui = &UI_multiplexer::instance(); //FIXME always use singleton
}


Filetransfer::~Filetransfer()
{
}


// TODO const TransportMessage* msg, dass klar ist wer delete aufruft
void Filetransfer::handleMessage(TransportMessage* msg, TransportMessage::MESSAGE_TYPE type, uint8_t subtype, UserPtr user)
{
  switch ( type )
  {
    // RECEIVER: got Payload Message
    case (TransportMessage::Transfer_Payload):
    {
        switch ( subtype )
        {
            case Filetransfer::DATA:
            {
                got_payload(msg, user);
                break;
            }

            case Filetransfer::ACK:
            {
                got_payload_ack(msg, user);
                break;
            }

            default:
            {
                ui->write_warn("Payload of unknown subtype.");
            }
        }

        break;
    }

    // Negotiation Message
    case (TransportMessage::Transfer_Negotiation):
    {
      switch ( subtype )
      {
        // PUSH-RECEIVER: someone wants to push us a file
        case Filetransfer::PUSH_REQUEST:
        {
            got_push_request(msg, user);
            break;
        }

        // PUSH-SENDER: got an «accept» for a file we want to push -> starting send-loop
        case Filetransfer::ACCEPT:
        {
            got_accept(msg, user);
          break;
        }

        // PUSH-SENDER: got a «decline» for a file we want to push -> cancel or renegotiate
        case Filetransfer::DECLINE:
        {
          DeclineMessage* m = msg->decapsulate<DeclineMessage>();

          if (! m->get_accept()) // double check
          {
            FilePtr f = get_file(outgoing_transfers, m->get_key());

            // OK: file in «outgoing» database
            if ( f )
            {
              ui->write_to_user("[FT_UP] File '" + f->get_filename() + "' declined. Cancel.");

              f->set_acceptance(false);
              remove_file(outgoing_transfers, f);
            }
            // ERR: this should never happen (probably we canceled the transfer)
            else
            {
              ui->write_info("Got decline for a file we don't want to push.");
            }
          }

          delete m;
          break;
        }

        // PUSH-RECEIVER: got an «abort» for a file transfer
        case Filetransfer::ABORT:
        {
          DeclineMessage* m = msg->decapsulate<DeclineMessage>();

          if (! m->get_accept()) // double check
          {
            FilePtr f = get_file(incoming_transfers, m->get_key());

            // OK: file in «outgoing» database
            if ( f )
            {
              ui->write_to_user("[FT_UP] File transfer of '" + f->get_filename() + "' aborted.");

              // TODO was wenn schon daten empfangen wurden? löschen?
              f->set_acceptance(false);
              f->file_close();

              remove_file(incoming_transfers, f);
            }
            // ERR: this should never happen (probably we canceled the transfer)
            else
            {
              ui->write_info("Got decline for a file we don't want to push.");
            }
          }

          delete m;
          break;
        }

        case Filetransfer::CUSTODY_REQUEST:
        {
            got_custody_request(msg, user);
            break;
        }

        case Filetransfer::PAUSE_NOTIFICATION:
        {
            got_pause_notification(msg, user);
            break;
        }

        case Filetransfer::DEMAND_PAUSE:
        {
            got_pause_demand(msg, user);
            break;
        }

        case Filetransfer::DEMAND_RESUME:
        {
            got_resume_demand(msg, user);
            break;
        }

        case Filetransfer::RESUME_REQUEST:
        {
            got_resume_request(msg, user);
            break;
        }

        // PUSH-RECEIVER: got transfer finished notification
        case Filetransfer::FINISHED:
        {
          AcceptMessage* m = msg->decapsulate<AcceptMessage>();

          if (m->get_accept()) // double check
          {
            FilePtr f = get_file(incoming_transfers, m->get_key());

            // OK: file in «incoming» database
            if ( f )
            {
              f->set_acceptance(false);
              f->file_close(); // TODO direkt close aufrufen ist doof. dass soll fileinfo automatisch machen..

              // ACK
              AcceptMessage msg(true, f->get_key(), f->get_transferred());  // TODO das 3. brauchen wir eigentlich gar nicht.
              trans->send_to(msg, TransportMessage::Transfer_Negotiation, Filetransfer::FINISHED_ACK, f->get_partner());

              // remove FileInfo
              remove_file(incoming_transfers, f);
              f->set_finished();
              finished_transfers.push_front(f);

              ui->write_to_user("[FT_UP] FILETRANSFER: " + f->get_filename() + " FINISHED");
            }
            else
            {
              ui->write_warn("WHAT THE.. ** ??"); // das lassen wir mal ne weile hier :-P
            }
          }
          else
          {
            ui->write_warn("FILETRANSFER: " + m->get_key() + " something went wrong.");
          }

          delete m;
          break;
        }


        // SENDER: got transfer finished ack
        case Filetransfer::FINISHED_ACK:
        {
          AcceptMessage* m = msg->decapsulate<AcceptMessage>();

          if (m->get_accept()) // double check
          {
            FilePtr f = get_file(outgoing_transfers, m->get_key());

            // OK: file in «outgoing» database
            if ( f )
            {
              // remove FileInfo
              remove_file(outgoing_transfers, f);
              f->set_finished();
              f->file_close();  // XXX irgendwie ist echt unklar, wann die files tatsächlich geschlossen werden.. aber das hier ist offensichtlich nötig...
              finished_transfers.push_front(f);

              ui->write_to_user("[FT_UP] FILETRANSFER: " + f->get_filename() + " FINISHED");

              // Transfer-Bundle callback
              TransferBundlePtr bundle = f->get_bundle();
              if ( bundle )
              {
                  bundle->on_transfer_finished(f);
              }

              // custody, download once
              if ( f->get_partner()->file_has_privilege(f->get_path(), Access_attributes::DOWNLOAD_ONCE) )
              {
                  ui->write_info("DELETE: " + f->get_path()); // XXX nur debug..
                  boost::filesystem::remove(f->get_path());
              }
            }
            else
            {
              ui->write_warn("WHAT THE.. ** ??"); // das lassen wir mal ne weile hier :-P
            }
          }
          else
          {
            ui->write_warn("FILETRANSFER: " + m->get_key() + " something went wrong.");
          }

          delete m;
          break;
        }

        // LOOK, queried user
        case (Filetransfer::LOOK):
        {
            ChatMessage* m = msg->decapsulate<ChatMessage>();

//            ui->write_info("**got something");
            ptree pt;
            pt.add("partner", users->get_myself()->get_name());
            user->list_accessible_files(pt, m->chatText);

            ptree pt_top;
            pt_top.add_child("accessible_files", pt);

            ostringstream out;
            boost::property_tree::write_xml(out, pt_top);

            ChatMessage reply(out.str());
            trans->send_to(reply, TransportMessage::Transfer_Negotiation, Filetransfer::LOOK_REPLY, user);

            delete m;
            break;
        }

        // LOOK, querying user
        case (Filetransfer::LOOK_REPLY):
        {
            ChatMessage* m = msg->decapsulate<ChatMessage>();

//            ui->write_info("ACCESSIBLE FILES on '" + user->get_name() + "':\n" + m->chatText);
            ui->write_to_user(m->chatText);

            delete m;
            break;
        }

        // PULL-SENDER: someone wants to get a file
        case Filetransfer::PULL_REQUEST:
        {
            got_pull_request(msg, user);
            break;
        }

        // PULL-RECEIVER: pull-request confirmed
        case Filetransfer::PULL_ACCEPT:
        {
            PullAcceptMessage* m = msg->decapsulate<PullAcceptMessage>();

            if (m->get_accept()) // double check
            {
              FilePtr f = get_file(incoming_transfers, m->get_key());

              // OK: file in «incoming» database
              if ( f )
              {
                f->set_size(m->get_size());
                f->set_acceptance(true);
                ui->write_to_user("[FT_UP] pull accepted (" + f->get_filename() + ")");
              }
              else
              {
                ui->write_warn("Key '" + m->get_key() + "' NOT in incoming database!!\n");
                status();
              }
            }
            else
            {
              ui->write_warn("FILETRANSFER: " + m->get_key() + " something went wrong.");
            }


            delete m;
            break;
        }


        case Filetransfer::BUNDLE_INFO:
        {
            got_bundle_info(msg, user);
            break;
        }


        // BUG reproducer..
        case Filetransfer::BUGS:
        {
            got_bugs(msg, user);
            break;
        }


        default:
        {
          ui->write_warn("[Filetransfer] Error: unknown message subtype");
        }
      }

      break;
    }

    default:
    {
      ui->write_warn("[Filetransfer] Error: unknown message type");
      
      break;
    }
  }

  delete msg;
}


// PUSH-SENDER: trigger a PUSH operation
void Filetransfer::push_file(UserPtr dest, string path)
{
//    // assert: is_connected
//    if ( ! dest->is_connected() )
//    {
//        ui->write_info("please connect first!");
//        return;
//    }

    boost::filesystem::path xpath(path);
    if ( boost::filesystem::is_regular_file(xpath) )
    {
      string filename = xpath.filename().string();  // BOOST 1.42: xpath.filename()
      string key = create_unique_key(filename);
      uint64_t size = boost::filesystem::file_size(xpath);  // FIXME könnte blockieren..

      ostringstream out;
      out << "[FT_UP] PUSH file: '" << filename << "' to " + dest->get_name() << endl
          << ">> " << " SIZE: " << size << " [waiting for acceptance]";
      ui->write_to_user(out.str());

      FilePtr f(new FileInfo(key, filename, size, dest));
      f->set_path(path);
      bool success = add_file(outgoing_transfers, f);

      if ( success )
      {
        PushRequestMessage msg(f);
//        trans->send_to(msg, TransportMessage::Transfer_Negotiation, Filetransfer::PUSH_REQUEST, dest);
        
        // XXX testing...
        trans->send_eventually(msg, TransportMessage::Transfer_Negotiation, Filetransfer::PUSH_REQUEST, dest, "TEST!!");
      }
      else
      {
        ui->write_warn("Error! Can't push file.");
      }
    }
    else
    {
      ui->write_warn("File: '" + path + "' does not exist (or is no regular file)!");
    }
}


// PUSH-RECEIVER: accept incoming file [convenience function - UI]
void Filetransfer::accept_file(string key)
{
    // resolve key
    FilePtr file = get_file(incoming_transfers, key);

    // OK: key in incoming database
    if ( file )
    {
        ui->write_to_user("[FT_UP] ACCEPT: '" + file->get_filename() + "' from: " + file->get_partner()->get_name());

        // call direct function
        string localpath = construct_incoming_path(file);
        accept_file(file, localpath);
    }
    // ERR: key not found, typo?
    else
    {
      ui->write_warn("WARN: key '" + key + "' not found.");
    }
}


// PUSH-RECEIVER: accept incoming file [direct function]
void Filetransfer::accept_file(FilePtr file, string path)
{
    uint64_t skip = 0;

    if ( boost::filesystem::exists(path) )
    {
        skip = boost::filesystem::file_size(path);
    }

    file->set_path(path);
    file->skip(skip);
    file->set_acceptance(true);

    AcceptMessage msg(true, file->get_key(), skip);
    trans->send_eventually(msg, TransportMessage::Transfer_Negotiation, Filetransfer::ACCEPT, file->get_partner(), "testing...");
    UI.write_debug("ACCEPT FILE: " + file->get_filename());
}

// PUSH-RECEIVER: decline incoming file
void Filetransfer::decline_file(string key)
{
    // resolve key
    FilePtr file = get_file(incoming_transfers, key);

    // OK: key in incoming database
    if ( file )
    {
        ui->write_to_user("[FT_UP] DECLINE: '" + file->get_filename() + "' from: " + file->get_partner()->get_name());

        DeclineMessage m(false, DeclineMessage::USER, file->get_key());
        trans->send_eventually(m, TransportMessage::Transfer_Negotiation, Filetransfer::DECLINE, file->get_partner(), "testing...");
        remove_file(incoming_transfers, file);
    }
    // ERR: key not found, typo?
    else
    {
      ui->write_warn("WARN: key '" + key + "' not found.");
    }
}

// SENDER: abort outgoing filetransfer
void Filetransfer::abort_transfer(string key)
{
    // resolve key
    FilePtr file = get_file(outgoing_transfers, key);

    // OK: key in incoming database
    if ( file )
    {
        ui->write_to_user("[FT_UP] ABORT transfer of: '" + file->get_filename() + "' to: " + file->get_partner()->get_name());

        DeclineMessage m(false, DeclineMessage::USER, file->get_key());
        trans->send_eventually(m, TransportMessage::Transfer_Negotiation, Filetransfer::ABORT, file->get_partner(), "testing...");
        file->set_acceptance(false);
        remove_file(outgoing_transfers, file);
    }
    // ERR: key not found, typo?
    else
    {
      ui->write_warn("WARN: key '" + key + "' not found.");
    }
}

// SENDER: pause outgoing filetransfer
void Filetransfer::pause_transfer(string key, bool just_hold)
{
    // resolve key
    FilePtr file = get_file(outgoing_transfers, key);

    // OK: key in incoming database
    if ( file )
    {
        ostringstream out;
        out << "[FT_UP] PAUSE transfer of: '" << file->get_filename() << "' to: " << file->get_partner()->get_name() << " POS: " << file->get_transferred();
        ui->write_to_user(out.str());

        file->pause(just_hold);
        PauseMessage msg(PauseMessage::PAUSE, file->get_key(), file->get_transferred());
        trans->send_to(msg, TransportMessage::Transfer_Negotiation, Filetransfer::PAUSE_NOTIFICATION, file->get_partner());
    }
    // ERR: key not found, typo?
    else
    {
      ui->write_warn("WARN: key '" + key + "' not found.");
    }
}

// RECEIVER: request pause for incoming filetransfer
void Filetransfer::demand_pause(string key)
{
    // resolve key
    FilePtr f = get_file(incoming_transfers, key);

    // OK: key in incoming database
    if ( f )
    {
        // call raw function. just_hold=false;
        demand_pause(f, false);
    }
    // ERR: key not found, typo?
    else
    {
      ui->write_warn("WARN: key '" + key + "' not found.");
    }
}

// raw ^^
void Filetransfer::demand_pause(FilePtr file, bool just_hold)
{
    ui->write_info("REQUEST PAUSE transfer of: '" + file->get_filename() + "' to: " + file->get_partner()->get_name());

    PauseMessage::PAUSE_STATE p = PauseMessage::PAUSE;
    if ( just_hold )
        p = (PauseMessage::PAUSE_STATE) (p | PauseMessage::HOLD);
    PauseMessage m(p, file->get_key(), file->get_transferred());  // soll die position von gegenseite ausgewertet werden?
    trans->send_to(m, TransportMessage::Transfer_Negotiation, Filetransfer::DEMAND_PAUSE, file->get_partner());
}


// SENDER: continue paused outgoing filetransfer
void Filetransfer::resume_transfer(string key)
{
    // resolve key
    FilePtr file = get_file(outgoing_transfers, key);

    // OK: key in incoming database
    if ( file )
    {
        // call raw function. just_hold=false;
        resume_transfer(file, false);
    }
    // ERR: key not found
    else
    {
      ui->write_warn("WARN: key '" + key + "' not found.");
    }
}

// raw ^^
void Filetransfer::resume_transfer(FilePtr file, bool just_hold)
{
    ostringstream out;
    out << "Trying to resume transfer of '" << file->get_filename() << "' to '" << file->get_partner()->get_name() << "' (at position: " << file->get_transferred()  << ")";
    ui->write_info(out.str());

    PauseMessage::PAUSE_STATE p = PauseMessage::PAUSE;
    if ( just_hold )
        p = (PauseMessage::PAUSE_STATE) (p | PauseMessage::HOLD);
    PauseMessage msg(p, file->get_key(), file->get_transferred());
    trans->send_to(msg, TransportMessage::Transfer_Negotiation, Filetransfer::RESUME_REQUEST, file->get_partner());
}


// RECEIVER: demand continuation of a paused incoming filetransfer
void Filetransfer::demand_resume(string key)
{
    // resolve key
    FilePtr file = get_file(incoming_transfers, key);

    // OK: key in incoming database
    if ( file )
    {
        ostringstream out;
        out << "[FT_UP] RESUME: " << file->get_key();
        ui->write_to_user(out.str());

        file->set_pause(false);
        PauseMessage msg(PauseMessage::RESUME, key, file->get_transferred());
        trans->send_to(msg, TransportMessage::Transfer_Negotiation, Filetransfer::DEMAND_RESUME, file->get_partner());
    }
    // ERR: key not found, typo?
    else
    {
      ui->write_warn("WARN: key '" + key + "' not found.");
    }
}


// FIXME: Keys müssen nicht eindeutig sein...
// BOTH: open transferred file
void Filetransfer::open_file(string key)
{
//    // resolve key
//    FilePtr file = get_file(finished_transfers, key);
//
//    // OK: key in incoming database
//    if ( file )
//    {
//        string command("xdg-open " + file->get_path());
//        system(command.c_str());
//    }
//    // ERR: key not found, typo?
//    else
//    {
//      ui->write_warn("WARN: key '" + key + "' not found.");
//    }
}

// TODO: "incoming/" nicht hardcodieren..
// XXX LINUX ONLY
void Filetransfer::open_incoming_dir()
{
    int i = system("xdg-open incoming/");
}


void Filetransfer::look(UserPtr user, string dir)
{
    // assert: is_connected
    if ( ! user->is_connected() )
    {
        ui->write_info("please connect first!");
        return;
    }

    // TODO anständige message-klasse
    ChatMessage m(dir);
    trans->send_to(m, TransportMessage::Transfer_Negotiation, Filetransfer::LOOK, user);
}

// PULL-RECEIVER (initiator) - request pull
void Filetransfer::pull_file(UserPtr user, string path)
{
    // assert: is_connected
    if ( ! user->is_connected() )
    {
        ui->write_info("please connect first!");
        return;
    }

    // filename & path
    boost::filesystem::path xpath(path);
    string filename = xpath.filename().string();  // BOOST 1.47: xpath.filename().string()
    string key = create_unique_key(filename);
    FilePtr f(new FileInfo(key, filename, 0, user));
    string localpath = construct_incoming_path(f);

    // file already exists -> abort.   // TODO automatisch umbenennen, wiederaufnahme?
    if ( boost::filesystem::is_regular_file(localpath) )
    {
        ui->write_warn("File '" + filename + "' already exists!");
        boost::filesystem::remove(localpath);
        ui->write_info(" --> deleted!");
    }

    // FileInfo
    f->set_path(localpath);
    f->set_acceptance(false);  //TODO das ist jetzt die frage, wann man das auf true setzt.. (Semantik von acceptance)
    add_file(incoming_transfers, f);

//    ui->write_info("pull request for: '" + key + "', '" + filename + "', '" + localpath + ", '" + path + "'");

    // send Pull-Request
    PullRequestMessage m(key, path);
    trans->send_to(m, TransportMessage::Transfer_Negotiation, Filetransfer::PULL_REQUEST, user);
}


// PUSH-SENDER: trigger a PUSH operation
void Filetransfer::upload_to_server(UserPtr server, UserPtr receiver, string path)
{
    // assert: is_connected
    if ( ! server->is_connected() )
    {
        ui->write_info("please connect first!");
        return;
    }

    boost::filesystem::path xpath(path);
    if ( boost::filesystem::is_regular_file(xpath) )
    {
        string filename = xpath.filename().string();  // BOOST 1.47: xpath.filename().string()
        string key = create_unique_key(filename);
        uint64_t size = boost::filesystem::file_size(xpath);

        ostringstream out;
        out << "[FT_UP] UPLOAD file: '" << filename << "' for " + receiver->get_name() << " to " << server->get_name() << endl
          << ">> " << " SIZE: " << size << " [waiting for acceptance]";
        ui->write_to_user(out.str());

        FilePtr f(new FileInfo(key, filename, size, server));
        f->set_path(path);
        bool success = add_file(outgoing_transfers, f);

        if ( success )
        {
            CustodyRequestMessage m(f, receiver->get_name());
            trans->send_to(m, TransportMessage::Transfer_Negotiation, Filetransfer::CUSTODY_REQUEST, server);
        }
        else
        {
            ui->write_warn("Error! Can't upload file.");
        }
    }
    else
    {
        ui->write_warn("File: '" + path + "' does not exist!");
    }
}






/** PRIVATE **/
/** handler for incoming packets **/
/* receiver */
void Filetransfer::got_payload(TransportMessage* msg, UserPtr user)
{
    PayloadMessage* m = msg->decapsulate<PayloadMessage>();
    PayloadPtr pay = m->get_payload();

    // search file in incoming-database
    FilePtr file = get_file(incoming_transfers, pay->get_key());

    // OK: yes, we actually want this file
    if ( file && file->get_acceptance() == true )
    {
        if ( file->is_paused() )
        {
            ui->write_warn("Got payload from paused transfer '" + file->get_key() + "' (accept anyway)");
        }

        // XXX  wenn A != B ist die Datei kaputt  (komischer bug der ohne grund verschwunden ist..)
        // TODO go-back-n
        uint64_t A = file->get_transferred();
        uint64_t B = pay->get_postion();
        if ( A != B )
        {
            ostringstream out;
            out << "ERROR !! Expected: " << A << ", GOT: " << B;
            ui->write_warn(out.str());

            decline_file(pay->get_key());
        }

      // OK: payload size seems sane  // TODO BUFFSIZE ist nichtmehr der gültige wert..
      if ( pay->get_size() <= BUFFSIZE && pay->get_size() > 0) // zur sicherheit..
      {
        file->append((char*) pay->raw_pointer(), pay->get_size());
      }
      // ERR: size variable corrupted
      else
      {
          ostringstream out;
          out << "WARNING!! Filepayload too large (or zero)! (" << pay->get_size() << ") Packet corrupted?";
          ui->write_warn(out.str());
      }

      // XXX testing... FlowControl ACKs -- Stop & Wait at the moment
      AcceptMessage ack(true, pay->get_key(), pay->get_postion());
      trans->send_to(ack, TransportMessage::Transfer_Payload, Filetransfer::ACK, file->get_partner());
    }
    // ERR: Protocol violation
    else
    {
      ui->write_warn("Error! Got payload for a file we didn't accept. (" + pay->get_key() + ")");
    }

    delete m;
}

// XXX FlowControl ACKS
void Filetransfer::got_payload_ack(TransportMessage* msg, UserPtr user)
{
    AcceptMessage* m = msg->decapsulate<AcceptMessage>();

    FilePtr file = get_file(outgoing_transfers, m->get_key());

//    ui->write_to_user("got pay-ack");
//    for (int i=0; i<10; i++)
    if ( file )
        file->request_async_data(this, file);

    delete m;
}


void Filetransfer::got_push_request(TransportMessage* msg, UserPtr user)
{
    PushRequestMessage* m = msg->decapsulate<PushRequestMessage>();

    FilePtr f(new FileInfo(m->get_key(), m->get_file_name(), m->get_size(), user));
    f->set_acceptance(false);
    // TODO, Achtung !! hier müssen die keys synchronisiert werden !!
    // TODO => siehe own_key, foreign_key...    neee, vgl Sync-tags.
    bool inserted = add_file(incoming_transfers, f);

    if ( inserted )
    {
        /* ui output */
        stringstream out;

        out << "[FT_UP] PUSH REQUEST from " << user->get_name() << ":\n>> File: '" << m->get_file_name() << "'\n>> Size: "
            << m->get_size();

        // check privileges
        if ( user->check_privilege_push() )
        {
            // ACCEPT
            string localpath = construct_incoming_path(f);
            accept_file(f, localpath);

            out << " -> AUTOACCEPT.";
            ui->write_to_user(out.str());
        }
        else
        {
            // ASK
            out << endl << ">> accept with: ACPT " << f->get_key();
            ui->write_to_user(out.str());
        }
    }
    else
    {
        // TODO decline, oder gar was sinnvolles.. (noch schön machen..)
        ui->write_warn("Key already used. (" + m->get_key() + ")");
        DeclineMessage ans(false, DeclineMessage::USER, m->get_key());
        trans->send_to(ans, TransportMessage::Transfer_Negotiation, Filetransfer::DECLINE, user);
    }

    delete m;
}


// SERVER
void Filetransfer::got_custody_request(TransportMessage* msg, UserPtr user)
{
    CustodyRequestMessage* m = msg->decapsulate<CustodyRequestMessage>();

    FilePtr f(new FileInfo(m->get_key(), m->get_file_name(), m->get_size(), user));
    f->set_acceptance(false);
    // TODO, Achtung !! hier müssen die keys synchronisiert werden !!
    bool inserted = add_file(incoming_transfers, f);

    if ( inserted )
    {
        /* ui output */
        stringstream out;

        out << "CUSTODY REQUEST from " << user->get_name() << " for " << m->get_receiver() << ":\n>> File: '" << m->get_file_name() << "'\n>> Size: "
            << m->get_size();


        // check privileges
        if ( user->check_privilege_custody() )
        {
            // find receiver in database
            UserPtr receiver = users->getUser(m->get_receiver());

            if ( receiver )
            {
                // path
                string path = "custody/" + user->get_masked_name() + "/to_" + receiver->get_masked_name() + "/";
                if ( ! boost::filesystem::is_directory(path))
                    boost::filesystem::create_directories(path); //TODO abbrechen wenn fehlgeschlagen

                // set access rights for receiver
                if ( !receiver->file_has_privilege(path, Access_attributes::PULL) )
                    receiver->grant_file_permissions(path, Access_attributes::PULL | Access_attributes::DOWNLOAD_ONCE);

                // path incl filename
                path += m->get_file_name();

                // ACCEPT
                accept_file(f, path);

                out << " -> AUTOACCEPT.";
                ui->write_info(out.str());

            }
            else
            {
                // TODO cancel, reason: unknown receiver (noch schön machen..)
                remove_file(incoming_transfers, f);
                DeclineMessage m(false, DeclineMessage::USER, f->get_key());
                trans->send_to(m, TransportMessage::Transfer_Negotiation, Filetransfer::DECLINE, user);
            }
        }
        else
        {
            // TODO cancel, reason: no custody privilege (noch schön machen..)
            remove_file(incoming_transfers, f);
            DeclineMessage m(false, DeclineMessage::USER, f->get_key());
            trans->send_to(m, TransportMessage::Transfer_Negotiation, Filetransfer::DECLINE, user);
        }
    }
    else
    {
        // TODO decline (noch schön machen..)
        ui->write_warn("Key already used. (" + m->get_key() + ")");
        DeclineMessage m(false, DeclineMessage::USER, f->get_key());
        trans->send_to(m, TransportMessage::Transfer_Negotiation, Filetransfer::DECLINE, user);
    }

    delete m;
}


void Filetransfer::got_accept(TransportMessage* msg, UserPtr user)
{
    AcceptMessage* m = msg->decapsulate<AcceptMessage>();

    if (m->get_accept()) // double check
    {
        start_resume_transfer_from(m->get_key(), m->get_skip(), false);
    }

    delete m;
}


void Filetransfer::got_pause_notification(TransportMessage* msg, UserPtr user)
{
    PauseMessage* m = msg->decapsulate<PauseMessage>();

    FilePtr f = get_file(incoming_transfers, m->get_key());

    // OK: file in «incoming» database
    if ( f )
    {
        // TODO das ist ein incoming file, hier funktioniert pause anders als sonst. -- es sollte zw Infiles und Outfiles unterschieden werden...
        f->set_pause(true);
        f->set_hold(m->is_hold());
        f->file_close();

        ostringstream out;
        out << "[FT_UP] PAUSED: " << f->get_filename() << " at pos: " << m->get_position() << " (own position is: " << f->get_transferred() << ")";
        ui->write_to_user(out.str());
    }
    // ERR: this should never happen (probably we canceled the transfer)
    else
    {
        ui->write_warn("Error! Got pause notification for a file we don't know.");
    }

    delete m;
}

void Filetransfer::got_resume_request(TransportMessage* msg, UserPtr user)
{
    PauseMessage* m = msg->decapsulate<PauseMessage>();

    FilePtr f = get_file(incoming_transfers, m->get_key());

    // OK: file in «incoming» database
    if ( f )
    {
        // BRANCH: no! attempt to unhold a _paused_ transfer
        if ( m->is_hold() && ! f->get_try_resume() )
        {
            // TODO decline? oder kann man das einfach ignoriert lassen?
        }
        // BRANCH: okay, go ahead..
        else
        {
            ostringstream out;
            out << "[FT_UP] RESUME REQ: " << f->get_key() << "' at pos: " << m->get_position() << " (own position is: " << f->get_transferred() << ")";
            ui->write_to_user(out.str());

            f->set_pause(false);
            AcceptMessage m(true, f->get_key(), f->get_transferred());
            bool ok = trans->send_to(m, TransportMessage::Transfer_Negotiation, Filetransfer::ACCEPT, user);

            if ( ! ok )
            {
                ui->write_warn("User appears NOT CONNECTED. Is this a bug??");
                f->set_pause(true);
            }
        }
    }
    // ERR: this should never happen (probably we canceled the transfer)
    else
    {
        ui->write_warn("Error! Got resume request for a file we don't know.");
    }

    delete m;
}


/* sender */
void Filetransfer::got_pull_request(TransportMessage* msg, UserPtr user)
{
    PullRequestMessage* neg_msg = msg->decapsulate<PullRequestMessage>();

    ui->write_info("PULL REQUEST from '" + user->get_name() + "', path: " + neg_msg->get_path() + " (key: " + neg_msg->get_key() + ")");


    // OK: access granted
    if ( user->file_has_privilege(neg_msg->get_path(), Access_attributes::PULL) )
    {
        boost::filesystem::path xpath(neg_msg->get_path());

        // FILE transfer (no dir)
        if ( boost::filesystem::is_regular(xpath) )
        {
            // TODO keys synchronisieren

            string filename = xpath.filename().string();  // BOOST 1.47: xpath.filename().string()
            uint64_t size = boost::filesystem::file_size(xpath);

            ui->write_to_user("[FT_UP]  --> GO!  (filename: '" + filename + "')");

            FilePtr f(new FileInfo(neg_msg->get_key(), filename, size, user));
            f->set_path(neg_msg->get_path());
            add_file(outgoing_transfers, f);
            f->set_acceptance(true);

            // confirm pull request and file meta-data
            PullAcceptMessage m(true, size, neg_msg->get_key());
            trans->send_to(m, TransportMessage::Transfer_Negotiation, Filetransfer::PULL_ACCEPT, user);

            // begin transfer
//            boost::thread t(&Filetransfer::sendfile, this, f);
            sendfile(f);  // XXX testing...
        }
        // TODO else?

    }
    // ERR: access declined
    else
    {
        ui->write_info(" --> NO!");
        ; //TODO decline message
    }

    delete neg_msg;
}


void Filetransfer::got_pause_demand(TransportMessage* msg, UserPtr user)
{
    PauseMessage* m = msg->decapsulate<PauseMessage>();

    pause_transfer(m->get_key(), m->is_hold());

    delete m;
}

void Filetransfer::got_resume_demand(TransportMessage* msg, UserPtr user)
{
    PauseMessage* m = msg->decapsulate<PauseMessage>();

    start_resume_transfer_from(m->get_key(), m->get_position(), m->is_hold());

    delete m;
}


/* this is called when a disk read has finished and we can send the data */
void Filetransfer::data_receive_callback(UserPtr user, PayloadPtr pay)
{
    // FIXME so ist es sch***
    FilePtr file = get_file(outgoing_transfers, pay->get_key());
    if ( ! file )
        return;

    // FIXME bei resume wird 2x die gleiche dateneinheit gesendet !!      immernoch?
    ostringstream out;
    if ( pay->get_postion() != file->get_transferred() )
    {
        out << "Datacallback: pay->get_postion() == " << pay->get_postion() << "; file->get_transferred() == " << file->get_transferred();
        ui->write_warn(out.str());
        return;
    }
    else
    {
//        out << "data callback for: " << pay->get_postion();
//        ui->write_to_user(out.str());
        ;
    }

    file->inc_transferred(pay->get_size());

    PayloadMessage m(pay);
    bool okay = trans->send_to(m, TransportMessage::Transfer_Payload, Filetransfer::DATA, user);

    // OK: message in transit
    if ( okay )
    {
        // finished
        if ( file->get_transferred() == file->get_size() )
        {
            AcceptMessage msg(true, file->get_key(), file->get_transferred());
            trans->send_to(msg, TransportMessage::Transfer_Negotiation, Filetransfer::FINISHED, file->get_partner());

            // Transfer-Bundle, continue with next file
            TransferBundlePtr bundle = file->get_bundle();
            if ( bundle )
            {
                bundle_announce_files_waiting(bundle);
            }
        }
    }
    // ERR: failed to send message (partner is not connected)
    else
    {
        ui->write_warn("Sending of payload to '" + user->get_name() + "' canceled. User is not connected.");
    }

}


// SENDER: (re-)start filetransfer from position.
// (the other user must await this)
// (send finish if position == filesize)
void Filetransfer::start_resume_transfer_from(string key, uint64_t position, bool if_on_hold)
{
    // fetch file object from outgoing database
    FilePtr f = get_file(outgoing_transfers, key);

    // OK: file in «outgoing» database
    if ( f )
    {
        f->seek(position);

        // BRANCH: transfer already finished
        if ( position == f->get_size() )
        {
            AcceptMessage fin(true, f->get_key(), position);
            trans->send_to(fin, TransportMessage::Transfer_Negotiation, Filetransfer::FINISHED, f->get_partner());

            // TODO Code duplikat, in eigene funktion auslagern!
            // Transfer-Bundle, continue with next file
            TransferBundlePtr bundle = f->get_bundle();
            if ( bundle )
            {
                bundle_announce_files_waiting(bundle);
            }
        }
        // BRANCH: actually start / resume
        else
        {
            // BRANCH: no! attempt to unhold a _paused_ transfer
            if ( if_on_hold && ! f->get_try_resume() )
            {
                // TODO decline? oder kann man das einfach ignoriert lassen?
            }
            // BRANCH: okay, let's roll..
            else
            {
                ostringstream out;
                out << "[FT_UP] File '" << f->get_filename() << "' accepted. [skip: " << position << "] Beginning transfer...";
                ui->write_to_user(out.str());
                f->set_acceptance(true);
                f->set_pause(false);

                sendfile(f); // TODO umbenennen, ggf start_transfer oder so
            }
        }
    }
    // ERR: this should never happen (probably we canceled the transfer)
    else
    {
        ui->write_warn("Error! Got acceptance for a file we don't want to push. (" + key + ")");
    }

}

// SENDER: sending loop (to be started in own thread)  // ist jetzt ja gar keine loop mehr....
void Filetransfer::sendfile(FilePtr file)
{
    for ( int i = 0; i < unacked_pkg; i++ )
    {
        bool x = file->request_async_data(this, file);
        if ( !x )
            break;
    }
}


// RECEIVER: return where an incoming file will be saved
string Filetransfer::construct_incoming_path(FilePtr file)
{
    // TODO "incoming/" nicht hardcodieren

    // path
    string path = "incoming/" + file->get_partner()->get_masked_name();

    // create subdir, if not existing
    if ( ! boost::filesystem::exists(path) )
    {
        if ( ! boost::filesystem::create_directories(path) )
            ; // TODO throw exception -> abort transfer
    }

    // check filename against attacks (../ etc)  // TODO muss hier noch mehr gemacht werden?
    string name = file->get_filename();
    size_t pos;
    while ( (pos = name.find("/")) != string::npos )
      name.replace(pos, 1, "_");  // TODO filename ändern in "file" ?

    // TODO, was wenn's den Namen schon gibt?

    return path + "/" + name;
}


/* DATABASE: incoming- / outgoing */
string Filetransfer::create_unique_key(string suggestion)
{
    bool collision;
    string candidate = suggestion;
    int num = 0;

    do
    {
        collision = false;

        // test collision
        if ( get_file(incoming_transfers, candidate) )
            collision = true;
        else if ( get_file(outgoing_transfers, candidate) )
            collision = true;

        // change candidate
        if ( collision )
        {
            ostringstream out;
            out << suggestion << num;
            candidate = out.str();

            num++;
        }

    } while ( collision );

    return candidate;
}

bool Filetransfer::add_file(FileMap& database, FilePtr file)
{
    // try to insert new file
    pair<FileMap::iterator, bool> ret = database.insert( std::pair<string, FilePtr>(file->get_key(), file) );

//    // key already in use, try another -- TODO, ist das hier wirklich sinnvoll? sollte das nicht lieber draußen "von hand" passieren? - kann dann aber recht einfach übernommen werden
//    int i = 0;
//    while ( ! ret.second )
//    {
//        // change key
//        file->change_key(file->get_key() + "'");
//
//        // have another try
//        ret = database.insert( std::pair<string, FilePtr>(file->get_key(), file) );
//
//        // avoid infinity loop -> abort if this is going nowhere !!
//        if ( i++ > 10 )
//            break;
//    }

    return ret.second;
}

// find file in map
FilePtr Filetransfer::get_file(FileMap& database, string key)
{
  FileMap::iterator it;

  it = database.find(key);

  // key not found
  if ( it == database.end() )
    return not_found;

  return it->second;
}

// FIXME: siehe open_file
//// find file in list
//FilePtr Filetransfer::get_file(std::list<FilePtr>& database, string key)
//{
//  FileMap::iterator it;
//
//  it = database.find(key);
//
//  // key not found
//  if ( it == database.end() )
//    return not_found;
//
//  return it->second;
//}


void Filetransfer::remove_file(FileMap& database, FilePtr file)
{
    FileMap::iterator it;

    it = database.find(file->get_key());

    // OK: remove
    if ( it != database.end() )
    {
        FilePtr x = it->second;
        database.erase(it);
    }
    // ERR: key not found
    else
    {
        throw not_found_exception("The transfer you want to remove does not even exist!!");
    }
}


void Filetransfer::status()
{
    ptree pt;

    // incoming:
    ptree pt_in;
    status_map(pt_in, incoming_transfers);
    pt.add_child("incoming", pt_in);

    // outgoing:
    ptree pt_out;
    status_map(pt_out, outgoing_transfers);
    pt.add_child("outgoing", pt_out);

    // finished:
    ptree pt_fin;
    status_list(pt_fin, finished_transfers);
    pt.add_child("finished", pt_fin);

    ptree pt_top;
    pt_top.add_child("filetransfers", pt);
    ostringstream out;
    boost::property_tree::write_xml(out, pt_top);
//    return out.str();
    ui->write_to_user(out.str());
}

void Filetransfer::status_map(ptree &pt, FileMap &database)
{
    FileMap::iterator it;
    for ( it=database.begin() ; it != database.end(); it++ )
    {
        ptree pt_t;
        it->second->status(pt_t);
        pt.add_child("file", pt_t);
    }
}

void Filetransfer::status_list(ptree &pt, std::list<FilePtr> &database)
{
    FileList::iterator it;
    for ( it=database.begin() ; it != database.end(); it++ )
    {
        ptree pt_t;
        (*it)->status(pt_t);
        pt.add_child("file", pt_t);
    }
}


void Filetransfer::status_old()
{
    FileMap::iterator it;
    stringstream s;

    s << "Filetransfers:" << endl;

    // incoming:
    s << "INCOMING" << endl;
    for ( it=incoming_transfers.begin() ; it != incoming_transfers.end(); it++ )
    {
        s << it->second->status_old() << endl;
    }

    // incoming:
    s << endl << "OUTGOING" << endl;
    for ( it=outgoing_transfers.begin() ; it != outgoing_transfers.end(); it++ )
    {
        s << it->second->status_old() << endl;
    }

    // ui
    ui->write_to_user(s.str());
}

// user connect/disconnected callback
void Filetransfer::got_connection_update(UserPtr user, bool connected)
{
    // BRANCH: connect
    if ( connected )
    {
        // resume hold (or never paused) transfers
        for ( FileMap::iterator it=outgoing_transfers.begin() ; it != outgoing_transfers.end(); it++ )
        {
//            UserPtr u = it->second->get_partner();
            FilePtr file = it->second;
            if ( file->get_partner() == user && file->get_acceptance() )
            {
                if ( !file->is_paused() || file->get_try_resume() )
                {
                    ui->write_info("---> Resume: " + file->get_filename() + " (queued)");
                    SystemEventType QueueEvent("What does this string?");
                    ResumeTransferEvent* func = new ResumeTransferEvent(this, file, true); // FIXME, who deletes func?? -- memory leak! Okay.. worked around via "delete this", but that's not very nice...
                    SystemQueue::instance().scheduleEvent( SystemEvent( func, QueueEvent, NULL), 200 );

                    // FIXME: prüfen was passiert, wenn !file->is_paused() -- zB nach overlay crash. Unklar ob es dann tut was es soll..
                }
                else
                {
                    // XXX only debug output, remove when stable
                    ui->write_info("---> Don't Resume: " + file->get_filename());
                }
                // XXX only debug output, remove when stable
                ostringstream out;
                out << "Pause: " << file->is_paused() << ", Hold: " << file->get_try_resume();
                ui->write_info(out.str());
            }
        }

    }

    // BRANCH: disconnect
    else
    {
        // set paused bit -- no pause request because the disconnect message will do the trick, too
        hold_transfers_for(user);

        // XXX ich glaub das muss wieder weg:
        ostringstream out;
        out << "[FT_UP] USER DISCONNECTED..";
        ui->write_to_user(out.str());
    }
}

void Filetransfer::hold_transfers_for(UserPtr user)
{
    hold_in_db(outgoing_transfers, user);
    hold_in_db(incoming_transfers, user);
}

void Filetransfer::hold_in_db(FileMap &database, UserPtr user)
{
    for ( FileMap::iterator it=database.begin() ; it != database.end(); it++ )
    {
        if ( it->second->get_partner() == user )
            it->second->hold();
    }
}

void Filetransfer::pause_all_transfers(bool just_hold)
{
    // outgoing
    for ( FileMap::iterator it=outgoing_transfers.begin() ; it != outgoing_transfers.end(); it++ )
    {
        FilePtr f = it->second;
        if ( ! f->is_paused() )
            f->pause(just_hold);
    }

    // incoming
    for ( FileMap::iterator it=incoming_transfers.begin() ; it != incoming_transfers.end(); it++ )
    {
        FilePtr f = it->second;
        if ( ! f->is_paused() )
            demand_pause(f, just_hold);
    }
}

void Filetransfer::pause_db(FileMap &database)
{
}


/* try to connect users we have transfers with */
void Filetransfer::connect_partners()
{
    // outgoing
    for ( FileMap::iterator it=outgoing_transfers.begin() ; it != outgoing_transfers.end(); it++ )
    {
        UserPtr u = it->second->get_partner();
        if ( ! u->is_connected() )
            trans->establish_connection(u);
    }
    // incoming
    for ( FileMap::iterator it=incoming_transfers.begin() ; it != incoming_transfers.end(); it++ )
    {
        UserPtr u = it->second->get_partner();
        if ( ! u->is_connected() )
            trans->establish_connection(u);
    }

}


void Filetransfer::save(ptree &pt)
{
    // incoming filetransfers
    boost::property_tree::ptree pt1;
    save_database(incoming_transfers, pt1);
    pt.add_child("incoming", pt1);

    // outgoing filetransfers
    boost::property_tree::ptree pt2;
    save_database(outgoing_transfers, pt2);
    pt.add_child("outgoing", pt2);
}

void Filetransfer::load(ptree &pt)
{
    // load from transfers.xml
    load_database(incoming_transfers, pt.get_child("incoming"));
    load_database(outgoing_transfers, pt.get_child("outgoing"));
}


void Filetransfer::save_database(FileMap &database, ptree &pt)
{
    for ( FileMap::iterator it=database.begin() ; it != database.end(); it++ )
    {
        boost::property_tree::ptree pt_u;
        it->second->save(pt_u);
        pt.add_child("file", pt_u);
    }
}


void Filetransfer::load_database(FileMap &database, ptree &pt)
{
    BOOST_FOREACH(ptree::value_type &v, pt)
    {
      FilePtr f(new FileInfo(v.second, users));
      cout << "TRANSFER: " + f->get_key() << endl;
      // FIXME: if ( f->get_partner() != users->NO_USER )
      add_file(database, f);
    }
}



/** Bundles **/
void Filetransfer::got_bundle_info(TransportMessage* msg, UserPtr user)
{
    BundleInfoMessage* m = msg->decapsulate<BundleInfoMessage>();

    bool okay = false;
    FilePtr file;

    // put information into FileInfo Object
    FilePtr f(new FileInfo());
    f->set_filename(m->get_displayname()); // TODO die Namen in BundleInfoMessage überdenken.. Was soll "displayname" bedeuten..?
    f->set_key(m->get_key());
    f->set_size(m->get_size());
    f->set_partner(user);
    f->set_acceptance(false);

    // notify bundle handler
    TransferBundleMap::iterator it;
    it = transfer_bundles.find(m->get_tag());
    if ( it != transfer_bundles.end() )
    {
        TransferBundlePtr bundle = it->second;

        file = bundle->on_incoming_transfer(f);
    }


    // all clear -> add to incoming-db
    if ( file )
        okay = add_file(incoming_transfers, file);


    // various error could have happen 'til here..
    if ( okay )
    {
        stringstream out;
        out << "[FT_UP] FILE from " << user->get_name() << " in BUNDLE " << m->get_tag() << ":\n>> Filename: '" << f->get_filename() << "'\n>> Size: " << m->get_size();

        /* If the bundle handler has accepted the transfer, no Accept Message is needed.
         * Otherwise the normal file-acceptance methods are used and an Accept-Message have to be sent.
         *
         * If the bundle handler declines a transfer, a null pointer is returned from bundle->on_incoming_transfer
         */
        if ( ! file->get_acceptance() )
        {
            // send explicit ACCEPT
            string localpath = construct_incoming_path(f);
            accept_file(f, localpath);

            out << " -> EXPLICIT ACCEPT.";
            ui->write_to_user(out.str());
        }
        else
        {
            out << " -> DIRECT ACCEPT.";
            out << "  (save to: '" << file->get_path() << "')";
            ui->write_to_user(out.str());
        }
    }
    else
    {
        ui->write_warn("Houston we have a problem! (" + m->get_key() + ")");
        DeclineMessage ans(false, DeclineMessage::USER, m->get_key());
        trans->send_to(ans, TransportMessage::Transfer_Negotiation, Filetransfer::DECLINE, user);
    }

    delete m;
}

bool Filetransfer::register_bundle(TransferBundlePtr bundle)
{
    pair<TransferBundleMap::iterator, bool> ret = transfer_bundles.insert( std::pair<string, TransferBundlePtr>(bundle->get_tag(), bundle) );

    return ret.second;
}


bool Filetransfer::bundle_announce_files_waiting(TransferBundlePtr bundle)
{
    bool success = false;
    FilePtr file = bundle->get_next_file();

    // BRANCH: got next file for this bundle
    if ( file )
    {
        ui->write_to_user("[FT_UP] BUNDLE: next file: " + file->get_path());
        ui->write_info("BUNDLE: next file: " + file->get_path());

        // prepare file object
        string key = create_unique_key(file->get_filename());
        file->set_key(key);
        success = add_file(outgoing_transfers, file);

        // notify receiver  (start immediately, if acceptance already handled by upper layer)
        if ( success )
        {
            BundleInfoMessage msg(key, bundle->get_tag(), file->get_filename(), "INVALID" /* TODO Pfad ist in Filename */, file->get_size(), ! file->get_acceptance());
            success &= trans->send_to(msg, TransportMessage::Transfer_Negotiation, Filetransfer::BUNDLE_INFO, file->get_partner());

            // start immediately
            if ( file->get_acceptance() )
            {
                ui->write_info("BUNDLE: beginning transfer immediately...");
                sendfile(file);
            }
        }

        if ( ! success )  // XXX das wäre ungut..
        {
            ui->write_warn("Error! Can't push file for bundle '" + bundle->get_tag() + "'.");
        }
    }
    // BRANCH: no more files in this bundle, at the moment -> notify bundle that we disabled it
    else
    {
        ui->write_info("BUNDLE: no next file.");

        bundle->on_disabled();
    }

    return success;
}

/** [Bundles] **/

///////////////////////////////////////////////////////////////////////////////

void Filetransfer::bug_reproduce(UserPtr user)
{
    AcceptMessage m(true, "hello buggy", 99);
    trans->send_to(m, TransportMessage::Transfer_Negotiation, Filetransfer::BUGS, user);
}

void Filetransfer::got_bugs(TransportMessage* msg, UserPtr user)
{
    AcceptMessage* m = msg->decapsulate<AcceptMessage>();

    ostringstream out;
    out << "GOT BUG-Message: " << m->get_accept() << ", " << m->get_key() << ", " << m->get_skip();
    ui->write_info(out.str());

    delete m;
}
